package wonder.tools;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;

/**
 * <p>Config2 class.</p>
 *
 * @author u211354
 * @version $Id: $
 */
public class Config2 implements Map<Object,Object> {
    /** Constant <code>PROPERTIES_TYPE=1</code> */
    public static final int PROPERTIES_TYPE = 1;
    /** Constant <code>JSON_TYPE=2</code> */
    public static final int JSON_TYPE = 2;

    private final Map<Object,Object> config;

    /**
     * Empty constructor.
     */
    public Config2() {
        config = new HashMap<Object,Object>();
    }

    /**
     * Loads a JSON or Properties file from the specified {@code File}.
     *
     * @param file a JSON or Properties file
     * @throws java.io.IOException if any
     */
    public void load(File file) throws IOException {
        if (file != null)
            load(file.toURI().toURL());
    }

    /**
     * Loads a specified JSON or Properties file from the specified {@code URL}.
     *
     * @param url a JSON or Properties file
     * @throws java.io.IOException if any
     */
    public void load(URL url) throws IOException {
        if (url != null) {
            InputStream in = null;
            try {
                in = url.openStream();
                int type = url.getPath().toLowerCase().endsWith(".json")
                        ? JSON_TYPE : PROPERTIES_TYPE;
                load(in, type);
            } finally {
                if (in != null) in.close();
            }
        }
    }

    /**
     * Loads a Properties file from the specified {@code InputStream}.
     *
     * @param in a Properties file
     * @throws java.io.IOException if any
     */
    public void load(InputStream in) throws IOException {
        load(in, PROPERTIES_TYPE);
    }

    /**
     * Loads a JSON or Properties file from the specified {@code InputStream}
     * and {@code type} using ISO 8859-1 character encoding.
     *
     * @param in a JSON or Properties file
     * @param type {@code PROPERTIES_TYPE} or {@code JSON_TYPE}
     * @throws java.io.IOException if any
     */
    public void load(InputStream in, int type) throws IOException {
        Reader reader = new InputStreamReader(in, "ISO-8859-1");
        reader = new BufferedReader(reader, 4096);
        load(reader, type);
    }

    /**
     * Loads a JSON or Properties file from the specified {@code Reader} and
     * {@code type}.
     *
     * @param in a JSON or Properties file
     * @param type {@code PROPERTIES_TYPE} or {@code JSON_TYPE}
     * @throws java.io.IOException if any
     */
    public void load(Reader in, int type) throws IOException {
        if (type == PROPERTIES_TYPE) {
            Properties props = new Properties();
            props.load(in);
            load(props);
        } else if (type == JSON_TYPE) {
            JsonFactory jsonFactory = new JsonFactory();
            jsonFactory.enable(JsonParser.Feature.ALLOW_COMMENTS);
            JsonParser jsonParser = jsonFactory.createJsonParser(in);

            ObjectMapper jsonMapper = new ObjectMapper();
            Map<String,Object> map = jsonMapper.readValue(jsonParser,
                    new TypeReference<Map<String,Object>>() {});
            config.putAll(map);
        } else {
            throw new IllegalArgumentException("Unknown type " + type);
        }
    }

    /**
     * Loads the entries from the specified {@code Map}.
     *
     * @param map entries
     */
    public void load(Map<Object,Object> map) {
        Set<Entry<Object,Object>> set = map.entrySet();
        for (Entry<Object,Object> entry : set)
            setEntry(entry.getKey(), entry.getValue());
    }

    /**
     * Returns true if this config contains an entry for the specified key.
     *
     * @param key a key
     * @return true if this config contains an entry for the specified key
     */
    public boolean containsEntry(Object key) {
        if (key == null) return false;
        if (key instanceof String)
            return containsEntry(config, (String)key);
        return config.containsKey(key);
    }

    /**
     * <p>containsEntry</p>
     *
     * @param obj a {@link java.lang.Object} object.
     * @param key a {@link java.lang.String} object.
     * @return a boolean.
     */
    protected boolean containsEntry(Object obj, String key) {
        int j = key.lastIndexOf('.');
        if (j >= 0) {
            obj = getEntry(key.substring(0, j));
            key = key.substring(j + 1);
        }

        try {
            if (obj instanceof Map) {
                Map<?,?> map = (Map<?,?>)obj;
                return map.containsKey(key);
            } else if (obj instanceof List) {
                List<?> list = (List<?>)obj;
                int i = Integer.parseInt(key);
                return i >= 0 && i < list.size();
            } else if (obj.getClass().isArray()) {
                int i = Integer.parseInt(key);
                return i >= 0 && i < Array.getLength(obj);
            } else {
                String s = "get" + key.substring(0, 1).toUpperCase()
                        + key.substring(1);
                obj.getClass().getMethod(s, (Class[])null);
                return true;
            }
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Returns the object to which the specified key is mapped or {@code null}
     * if this config contains no mapping for the key or if the key is
     * {@code null}.
     *
     * @param key a {@link java.lang.Object} object
     * @return a {@link java.lang.Object} object
     */
    public Object getEntry(Object key) {
        if (key == null) return null;
        if (key instanceof String)
            return getEntry(config, (String)key, 0);
        return config.get(key);
    }

    /**
     * <p>getEntry</p>
     *
     * @param obj a {@link java.lang.Object} object.
     * @param key a {@link java.lang.String} object.
     * @param i a int.
     * @return a {@link java.lang.Object} object.
     */
    protected Object getEntry(Object obj, String key, int i) {
        if (obj == null || key == null || i < 0) return obj;

        int j = key.indexOf('.', i);
        String subkey = j < 0 ? key.substring(i) : key.substring(i, j);

        try {
            if (obj instanceof Map) {
                Map<?,?> map = (Map<?,?>)obj;
                obj = map.get(subkey);
            } else if (obj instanceof List) {
                List<?> list = (List<?>)obj;
                obj = list.get(Integer.parseInt(subkey));
            } else if (obj.getClass().isArray()) {
                obj = Array.get(obj, Integer.parseInt(subkey));
            } else {
                String s = "get" + subkey.substring(0, 1).toUpperCase()
                        + subkey.substring(1);
                Method m = obj.getClass().getMethod(s, (Class[])null);
                obj = m.invoke(obj, (Object[])null);
            }
        } catch (Exception e) {
            return null;
        }

        return getEntry(obj, key, j < 0 ? j : j + 1);
    }

    /**
     * Associates the specified value with the specified key in this config. If
     * config map previously contained a mapping for the key, the old value is
     * replaced.
     *
     * @param key a {@link java.lang.Object} object
     * @param val a {@link java.lang.Object} object
     * @return a {@link java.lang.Object} object.
     */
    public Object setEntry(Object key, Object val) {
        if (val == null) return deleteEntry(key);
        if (key instanceof String)
            return setEntry(config, (String)key, 0, val);
        return config.put(key, val);
    }

    /**
     * <p>setEntry</p>
     *
     * @param obj a {@link java.lang.Object} object.
     * @param key a {@link java.lang.String} object.
     * @param i a int.
     * @param val a {@link java.lang.Object} object.
     * @return a {@link java.lang.Object} object.
     */
    public Object setEntry(Object obj, String key, int i, Object val) {
        if (key == null || i < 0) return val;

        int j = key.indexOf('.', i);
        String subkey = j < 0 ? key.substring(i) : key.substring(i, j);

        Object oldVal;
        if (j < 0) {
            if (obj instanceof Map) {
                @SuppressWarnings("unchecked")
                Map<Object,Object> map = (Map<Object,Object>)obj;
                oldVal = map.put(subkey, val);
            } else if (obj instanceof List) {
                @SuppressWarnings("unchecked")
                List<Object> list = (List<Object>)obj;
                i = Integer.parseInt(subkey);
                oldVal = list.set(i, val);
            } else if (obj.getClass().isArray()) {
                i = Integer.parseInt(subkey);
                oldVal = Array.get(obj, i);
                Array.set(obj, i, val);
            } else {
                oldVal = null;
            }
        } else {
            Object newObj = getEntry(obj, subkey, 0);
            if (newObj == null) {
                newObj = new HashMap<Object,Object>();
                @SuppressWarnings("unchecked")
                Map<Object,Object> map = (Map<Object,Object>)obj;
                map.put(subkey, newObj);
            }
            oldVal = setEntry(newObj, key, j < 0 ? j : j + 1, val);
        }
        return oldVal;
    }

    /**
     * Removes the mapping for the specified key from this config if present.
     *
     * @param key a {@link java.lang.Object} object
     * @return a {@link java.lang.Object} object.
     */
    public Object deleteEntry(Object key) {
        if (key == null) return null;
        if (key instanceof String)
            return deleteEntry(config, (String)key);
        return config.remove(key);
    }

    /**
     * <p>deleteEntry</p>
     *
     * @param obj a {@link java.lang.Object} object.
     * @param key a {@link java.lang.String} object.
     * @return a {@link java.lang.Object} object.
     */
    public Object deleteEntry(Object obj, String key) {
        int j = key.lastIndexOf('.');
        if (j >= 0) {
            obj = getEntry(key.substring(0, j));
            key = key.substring(j + 1);
        }

        if (obj instanceof Map) {
            Map<?,?> map = (Map<?,?>)obj;
            return map.remove(key);
        } else if (obj instanceof List) {
            List<?> list = (List<?>)obj;
            int i = Integer.parseInt(key);
            return list.remove(i);
        } else {
            return null;
        }
    }

    /**
     * <p>dispose</p>
     */
    public void dispose() {
        config.clear();
    }

    /**
     * <p>filter</p>
     *
     * @param str a {@link java.lang.String} object.
     * @return a {@link java.lang.String} object.
     * @throws java.lang.Exception if any.
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    protected String filter(String str) throws Exception {
        MessageTextTemplateVelocity mttv = new MessageTextTemplateVelocity();
        return mttv.getMessage(str, (Map)this);
    }

    /**
     * Returns the object to which the specified key is mapped converted to a
     * {@code String}.
     *
     * @param key a {@link java.lang.Object} object
     * @return a {@link java.lang.String} object
     */
    public String getString(Object key) {
        Object val = getEntry(key);
        if (val != null) return val.toString();
        return null;
    }

    /**
     * Returns the object to which the specified key is mapped converted to a
     * {@code Boolean}. Returns {@code null} if the value is not a
     * {@link Boolean}, a {@link String} or a {@link Number}.
     *
     * @param key a {@link java.lang.Object} object
     * @return a {@link java.lang.Boolean} object
     */
    public Boolean getBoolean(Object key) {
        Object val = getEntry(key);
        if (val != null) {
            if (val instanceof Boolean)
                return (Boolean)val;
            if (val instanceof String)
                return Boolean.valueOf((String)val);
            if (val instanceof Number)
                return Boolean.valueOf(((Number)val).longValue() == 0);
        }
        return null;
    }

    /**
     * Returns the object to which the specified key is mapped converted to a
     * {@code Integer}. Returns {@code null} if the value is not a
     * {@link Integer}, a {@link String} or a {@link Number}.
     *
     * @param key a {@link java.lang.Object} object
     * @return a {@link java.lang.Integer} object
     */
    public Integer getInteger(Object key) {
        Object val = getEntry(key);
        if (val != null) {
            if (val instanceof Integer)
                return (Integer)val;
            if (val instanceof String)
                return Integer.valueOf((String)val);
            if (val instanceof Number)
                return Integer.valueOf(((Number)val).intValue());
        }
        return null;
    }

    /**
     * Returns the object to which the specified key is mapped converted to a
     * {@code Long}. Returns {@code null} if the value is not a {@link Long}, a
     * {@link String} or a {@link Number}.
     *
     * @param key a {@link java.lang.Object} object
     * @return a {@link java.lang.Long} object
     */
    public Long getLong(Object key) {
        Object val = getEntry(key);
        if (val != null) {
            if (val instanceof Long)
                return (Long)val;
            if (val instanceof String)
                return Long.valueOf((String)val);
            if (val instanceof Number)
                return Long.valueOf(((Number)val).longValue());
        }
        return null;
    }

    /**
     * Returns the object to which the specified key is mapped converted to a
     * {@code Float}. Returns {@code null} if the value is not a {@link Float},
     * a {@link String} or a {@link Number}.
     *
     * @param key a {@link java.lang.Object} object
     * @return a {@link java.lang.Float} object
     */
    public Float getFloat(Object key) {
        Object val = getEntry(key);
        if (val != null) {
            if (val instanceof Float)
                return (Float)val;
            if (val instanceof String)
                return Float.valueOf((String)val);
            if (val instanceof Number)
                return Float.valueOf(((Number)val).floatValue());
        }
        return null;
    }

    /**
     * Returns the object to which the specified key is mapped converted to a
     * {@code Double}. Returns {@code null} if the value is not a {@link Double}
     * , a {@link String} or a {@link Number}.
     *
     * @param key a {@link java.lang.Object} object
     * @return a {@link java.lang.Double} object
     */
    public Double getDouble(Object key) {
        Object val = getEntry(key);
        if (val != null) {
            if (val instanceof Double)
                return (Double)val;
            if (val instanceof String)
                return Double.valueOf((String)val);
            if (val instanceof Number)
                return Double.valueOf(((Number)val).doubleValue());
        }
        return null;
    }

    /**
     * Returns the object to which the specified key is mapped converted to a
     * {@code IParametrable}. Returns {@code null} if the value is not a
     * {@link Map} or a {@link String}.
     *
     * @param key a {@link Object} object
     * @return a {@link IParametrable} object
     * @throws java.lang.Exception if an exception occurs
     */
    public IParametrable getParametrable(Object key) throws Exception {
        Object val = getEntry(key);
        if (val != null) {
            if (val instanceof Map) {
                @SuppressWarnings("unchecked")
                Map<String,Object> map = (Map<String,Object>)val;
                return ParameterHelper.newInstance(map);
            }
            if (val instanceof String)
                return ParameterHelper.newInstance((String)val);
        }
        return null;
    }

    /**
     * Returns a unmodifiable {@code Set} of the keys contained in this config.
     *
     * @return a {@link java.util.Set} object
     * @deprecated use {@link keySet} instead
     */
    @Deprecated
    public Set<Object> getAllKeys() {
        return Collections.unmodifiableSet(config.keySet());
    }

    /**
     * <p>getMap</p>
     *
     * @return a {@link java.util.Map} object.
     */
    protected Map<Object,Object> getMap() {
        return config;
    }

    /** {@inheritDoc} */
    @Override
    public int size() {
        return keySet().size();
    }

    /** {@inheritDoc} */
    @Override
    public boolean isEmpty() {
        return config.isEmpty();
    }

    /** {@inheritDoc} */
    @Override
    public boolean containsKey(Object key) {
        return containsEntry(key);
    }

    /** {@inheritDoc} */
    @Override
    public boolean containsValue(Object val) {
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public Object get(Object key) {
        return getEntry(key);
    }

    /** {@inheritDoc} */
    @Override
    public Object put(Object key, Object val) {
        return setEntry(key, val);
    }

    /** {@inheritDoc} */
    @Override
    public Object remove(Object key) {
        return deleteEntry(key);
    }

    /** {@inheritDoc} */
    @Override
    @SuppressWarnings("unchecked")
    public void putAll(Map<? extends Object,? extends Object> map) {
        load((Map<Object,Object>)map);
    }

    /** {@inheritDoc} */
    @Override
    public void clear() {
        config.clear();
    }

    /** {@inheritDoc} */
    @Override
    public Set<Object> keySet() {
        return Collections.unmodifiableSet(config.keySet());
    }

    /** {@inheritDoc} */
    @Override
    public Collection<Object> values() {
        return Collections.unmodifiableCollection(config.values());
    }

    /** {@inheritDoc} */
    @Override
    public Set<java.util.Map.Entry<Object,Object>> entrySet() {
        return Collections.unmodifiableSet(config.entrySet());
    }

    /**
     * <p>main</p>
     *
     * @param args an array of {@link java.lang.String} objects.
     */
    public static void main(String[] args) {
        Config2 c = new Config2();
        c.setEntry("A.B.C", "BonjourC");
        c.setEntry("A.B.D", "BonjourD");

        System.out.println(c.getEntry("A.B.C"));
        System.out.println(c.getEntry("A.B"));
    }
}
